"""
Security validation for notification service URLs.

This module provides validation for user-configured notification service URLs
to prevent Server-Side Request Forgery (SSRF) attacks and other security issues.
"""

import ipaddress
from typing import Optional, Tuple
from urllib.parse import urlparse
from loguru import logger


class NotificationURLValidationError(ValueError):
    """Raised when a notification service URL fails security validation."""

    pass


class NotificationURLValidator:
    """Validates notification service URLs to prevent SSRF and other attacks."""

    # Dangerous protocols that should never be used for notifications
    BLOCKED_SCHEMES = (
        "file",  # Local file access
        "ftp",  # FTP can be abused for SSRF
        "ftps",  # Secure FTP can be abused for SSRF
        "data",  # Data URIs can leak sensitive data
        "javascript",  # XSS/code execution
        "vbscript",  # XSS/code execution
        "about",  # Browser internal
        "blob",  # Browser internal
    )

    # Allowed protocols for notification services
    ALLOWED_SCHEMES = (
        "http",  # Webhook services
        "https",  # Webhook services (preferred)
        "mailto",  # Email notifications
        "discord",  # Discord webhooks
        "slack",  # Slack webhooks
        "telegram",  # Telegram bot API
        "gotify",  # Gotify notifications
        "pushover",  # Pushover notifications
        "ntfy",  # ntfy.sh notifications
        "matrix",  # Matrix protocol
        "mattermost",  # Mattermost webhooks
        "rocketchat",  # Rocket.Chat webhooks
        "teams",  # Microsoft Teams
        "json",  # Generic JSON webhooks
        "xml",  # Generic XML webhooks
        "form",  # Form-encoded webhooks
    )

    # Private IP ranges (RFC 1918 + loopback + link-local)
    PRIVATE_IP_RANGES = [
        ipaddress.ip_network("127.0.0.0/8"),  # Loopback
        ipaddress.ip_network("10.0.0.0/8"),  # Private
        ipaddress.ip_network("172.16.0.0/12"),  # Private
        ipaddress.ip_network("192.168.0.0/16"),  # Private
        ipaddress.ip_network("169.254.0.0/16"),  # Link-local
        ipaddress.ip_network("::1/128"),  # IPv6 loopback
        ipaddress.ip_network("fc00::/7"),  # IPv6 unique local
        ipaddress.ip_network("fe80::/10"),  # IPv6 link-local
    ]

    @staticmethod
    def _is_private_ip(hostname: str) -> bool:
        """
        Check if hostname resolves to a private IP address.

        Args:
            hostname: Hostname to check

        Returns:
            True if hostname is a private IP or localhost
        """
        # Check for localhost variations
        if hostname.lower() in (
            "localhost",
            "127.0.0.1",
            "::1",
            "0.0.0.0",
            "::",
        ):
            return True

        # Try to parse as IP address
        try:
            ip = ipaddress.ip_address(hostname)
            return any(
                ip in network
                for network in NotificationURLValidator.PRIVATE_IP_RANGES
            )
        except ValueError:
            # Not a valid IP address, might be a hostname
            # For security, we don't resolve hostnames to avoid DNS rebinding attacks
            # Apprise services should be configured with public endpoints
            return False

    @staticmethod
    def validate_service_url(
        url: str, allow_private_ips: bool = False
    ) -> Tuple[bool, Optional[str]]:
        """
        Validate a notification service URL for security issues.

        This function prevents SSRF attacks by validating that service URLs
        use safe protocols and don't target private/internal infrastructure.

        Args:
            url: Service URL to validate (e.g., "discord://webhook_id/token")
            allow_private_ips: Whether to allow private IPs (default: False)
                              Set to True for development/testing environments

        Returns:
            Tuple of (is_valid, error_message)
            - is_valid: True if URL passes security checks
            - error_message: None if valid, error description if invalid

        Examples:
            >>> validate_service_url("discord://webhook_id/token")
            (True, None)

            >>> validate_service_url("file:///etc/passwd")
            (False, "Blocked unsafe protocol: file")

            >>> validate_service_url("http://localhost:5000/webhook")
            (False, "Blocked private/internal IP address: localhost")
        """
        if not url or not isinstance(url, str):
            return False, "Service URL must be a non-empty string"

        # Strip whitespace
        url = url.strip()

        # Parse URL
        try:
            parsed = urlparse(url)
        except Exception as e:
            logger.warning(f"Failed to parse service URL: {e}")
            return False, f"Invalid URL format: {e}"

        # Check for scheme
        if not parsed.scheme:
            return False, "Service URL must have a protocol (e.g., https://)"

        scheme = parsed.scheme.lower()

        # Check for blocked schemes
        if scheme in NotificationURLValidator.BLOCKED_SCHEMES:
            logger.warning(
                f"Blocked unsafe notification protocol: {scheme} in URL: {url[:50]}..."
            )
            return False, f"Blocked unsafe protocol: {scheme}"

        # Check for allowed schemes
        if scheme not in NotificationURLValidator.ALLOWED_SCHEMES:
            logger.warning(
                f"Unknown notification protocol: {scheme} in URL: {url[:50]}..."
            )
            return (
                False,
                f"Unsupported protocol: {scheme}. "
                f"Allowed: {', '.join(NotificationURLValidator.ALLOWED_SCHEMES[:5])}...",
            )

        # For HTTP/HTTPS, check for private IPs (SSRF prevention)
        if scheme in ("http", "https") and not allow_private_ips:
            if parsed.hostname:
                if NotificationURLValidator._is_private_ip(parsed.hostname):
                    logger.warning(
                        f"Blocked private/internal IP in notification URL: "
                        f"{parsed.hostname}"
                    )
                    return (
                        False,
                        f"Blocked private/internal IP address: {parsed.hostname}",
                    )

        # Passed all security checks
        return True, None

    @staticmethod
    def validate_service_url_strict(
        url: str, allow_private_ips: bool = False
    ) -> bool:
        """
        Strict validation that raises an exception on invalid URLs.

        Args:
            url: Service URL to validate
            allow_private_ips: Whether to allow private IPs (default: False)

        Returns:
            True if valid

        Raises:
            NotificationURLValidationError: If URL fails security validation
        """
        is_valid, error_message = NotificationURLValidator.validate_service_url(
            url, allow_private_ips
        )

        if not is_valid:
            raise NotificationURLValidationError(
                f"Notification service URL validation failed: {error_message}"
            )

        return True

    @staticmethod
    def validate_multiple_urls(
        urls: str, allow_private_ips: bool = False, separator: str = ","
    ) -> Tuple[bool, Optional[str]]:
        """
        Validate multiple comma-separated service URLs.

        Args:
            urls: Comma-separated service URLs
            allow_private_ips: Whether to allow private IPs (default: False)
            separator: URL separator (default: ",")

        Returns:
            Tuple of (all_valid, error_message)
            - all_valid: True if all URLs pass validation
            - error_message: None if all valid, first error if any invalid
        """
        if not urls or not isinstance(urls, str):
            return False, "Service URLs must be a non-empty string"

        # Split by separator and strip whitespace
        url_list = [url.strip() for url in urls.split(separator) if url.strip()]

        if not url_list:
            return False, "No valid URLs found after parsing"

        # Validate each URL
        for url in url_list:
            is_valid, error_message = (
                NotificationURLValidator.validate_service_url(
                    url, allow_private_ips
                )
            )

            if not is_valid:
                # Return first error found
                return False, f"Invalid URL '{url[:50]}...': {error_message}"

        # All URLs passed validation
        return True, None
